import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_demo/pages/page_with_title.dart';
import 'package:flutter_demo/widgets/app_widgets.dart';

class CustomLayoutPage extends PageWithTitle {
  const CustomLayoutPage({super.key}) : super("自定义布局(LayoutBuilder、CustomSingleChildLayout、CustomMultiChildLayout)");

  @override
  List<Widget> buildContentWidgets(BuildContext context) {
    final items = List.generate(4, (index) {
      return Container(
        width: 30,
        height: 30,
        decoration: BoxDecoration(
            color: Theme.of(context).colorScheme.surfaceContainer,
            shape: BoxShape.circle),
        alignment: Alignment.center,
        child: Text("${index + 1}"),
      );
    });
    return [
      const PrimaryTitle("简介"),
      const BodyText("在现有的容器组件不足以满足布局需求时，可以通过自定义布局容器来实现"),
      const LabelText("1、布局也是组件，首要实现的就是测量并决定自身大小"),
      const LabelText("2、决定子组件位置，对子组件进行排布"),
      AppSpacer.large,
      const PrimaryTitle("在开始了解自定义布局前，我们先了解一下相关的辅助类"),
      const OlItem(1, "BoxConstraints"),
      const BodyText("约束框，用于表示组件尺寸应该受到的限制范围，内部提供了一些方法用于辅助常规的"
          "尺寸计算"),
      const CategoryTitle("常规构造"),
      const LabelText("- minWidth 最小宽度，表示组件宽度不能小于该值，double.infinity表示无限制"),
      const LabelText("- maxWidth 最大宽度"),
      const LabelText("- minHeight 最小高度"),
      const LabelText("- maxHeight 最大高度"),
      const CategoryTitle("命名构造"),
      const LabelText("- tight 紧缩，给定一个宽高值作为固定尺寸的约束"),
      const LabelText("- tightFor 给定宽高作为固定尺寸的约束，若为空则组件为自适应内容大小"),
      const LabelText("- tightForFinite 给定宽高作为固定尺寸的约束，若为double.infinity"
          "则组件为自适应内容大小"),
      const LabelText("- loose 松散，给定宽高作为最大值限制，组件可以自己从0到最大值选取一个合适的尺寸"),
      const LabelText("- expand 扩大，给定宽高作为固定尺寸的约束，若为空则对宽高不做任何约束"),
      const LabelText("- fromViewConstraints 拷贝视图的宽高约束到约束框中"),
      const CategoryTitle("通过方法创建新的约束框"),
      const LabelText("- deflate 按边距缩小宽高，得到一个减去边距后的约束框（这个应该是Padding组件用的吧）"),
      const LabelText("- loosen 最小值设置为零，得到一个只有最大值的约束框"),
      const LabelText("- enforce 强制满足另一个框的约束，会尽可能满足当前框的约束（若两者的"
          "限制区间有交集，就会表现为限制到只能取交集部分）"),
      const LabelText("- tighten 给定宽高，若宽高在限制范围内则约束为固定值，越界则约束为极限值，若为空则不做修改"),
      const LabelText("- flipped 翻转，对调宽高的限制"),
      const LabelText("- widthConstraints 仅约束宽度"),
      const LabelText("- heightConstraints 仅约束高度"),
      const CategoryTitle("计算"),
      const LabelText("- constrainWidth 给定一个宽度，通过宽度约束矫正后返回限制范围内的"
          "值（越界取极限值，不越界返回原始数值，通过clampDouble方法进行计算的）"),
      const LabelText("- constrainHeight 给定一个高度，通过高度约束矫正后返回限制范围内的值"),
      const LabelText("- constrain 给定一个宽高，返回经约束框矫正后的尺寸"),
      const LabelText("- constrainDimensions 同constrain"),
      const LabelText("- constrainSizeAndAttemptToPreserveAspectRatio 给定一个宽高"
          "，在满足约束框尺寸限制的前提下，会考虑原始的宽高比返回一个等比的尺寸"),
      const OlItem(2, "LayoutId"),
      const BodyText("布局组件id，用于在CustomMultiChildLayout中标记每个子组件，通过id来布局子组件"),
      AppSpacer.large,
      AspectRatio(
        aspectRatio: 16 / 9,
        child: Align(
          alignment: Alignment.bottomRight,
          child: SizedBox(
            height: 120,
            child: _LayoutBuilderExample(items),
          ),
        ),
      ),
      const UlItem("LayoutBuilder"),
      const BodyText("在自定义布局时，首先考虑通过组合现有组件封装成新的布局容器，这种方式实现"
          "较为简单，主要是封装逻辑，接用LayoutBuilder获取父组件给予的约束信息，决定自身尺寸"
          "，此组件也常用于根据父组件大小来实现响应式布局，为不同尺寸的容器提供不同布局方式"),
      const LabelText("- builder 在mount、update阶段会回调，此时我们返回要显示的组件，通"
          "过父组件给予的约束信息来决定我们组件大小，通常通过Stack+Positioned来实现计算后的"
          "组件位置摆放"),
      AppSpacer.large,
      AspectRatio(
        aspectRatio: 16 / 9,
        child: CustomSingleChildLayout(
          delegate: _CenterDelegate(),
          child: Container(
            width: 100,
            height: 100,
            color: Theme.of(context).colorScheme.primary,
          ),
        ),
      ),
      const UlItem("CustomSingleChildLayout"),
      const BodyText("Flutter许多布局参数类型的组件都是继承自SingleChildRenderObjectWidget，"
          "它允许包含单一的子组件，自定义单一子组件布局将RenderObject中关于尺寸获取、子组件约"
          "束创建、位置摆放等方法通过回调的方式暴露出来供外部实现，以此减轻自定义组件的复杂度"),
      const LabelText("- shouldRelayout 代理对象变化时回调，开发者根据代理对象参数是否发生"
          "变化来决定是否重新布局，这个只针对代理对象变化时是否需要重新布局，其它事件触发的重新"
          "布局是该干嘛干嘛，不受这个方法影响，方法名字也不起好点"),
      const LabelText("- getSize 根据父组件给的约束计算自身尺寸，默认取约束的最大值"),
      const LabelText("- getConstraintsForChild 获取子组件应该受到的约束，默认是直接使用"
          "父组件给的约束，根据实际情况可以对约束进行调整"),
      const LabelText("- getPositionForChild 获取子组件基于左上角的偏移量，也是我们实现"
          "摆放子组件的关键方法，默认不做任何偏移"),
      AppSpacer.large,
      AspectRatio(
        aspectRatio: 16 / 9,
        child: Align(
          alignment: Alignment.bottomRight,
          child: SizedBox(
            height: 120,
            child: CustomMultiChildLayout(
              //这里直接以序列为id
              delegate: _MobileGameSkillKeyboardDelegate(
                List.generate(items.length, (index) => index),
              ),
              children: List.generate(items.length, (index) {
                return LayoutId(id: index, child: items[index]);
              }),
            ),
          ),
        ),
      ),
      const UlItem("CustomMultiChildLayout"),
      const BodyText("Flutter许多容器类型的组件都是继承自MultiChildRenderObjectWidget，"
          "该组件专门用于多个子组件的容器实现，CustomMultiChildLayout基于它将RenderObject"
          "的关键方法暴露出来通过回调实现，对比CustomSingleChildLayout，显然自定义多子组件"
          "布局专门用于多个子组件的应用场景"),
      const NoticeText("- 所有的子组件均需要用LayoutId组件包裹，这方便在实现自定义布局时根据"
          "id来操作对应的子组件"),
      const NoticeText("- 理论上如果自定义布局排布子组件时无需考虑子组件大小的话，直接使用"
          "LayoutBuilder更方便，反之使用CustomMultiChildLayout性能消耗低，因为它在布局阶"
          "段已经通过layout测量得知子组件的尺寸，而LayoutBuilder需要预测量再重建为真正显示的组件"),
      const LabelText("- shouldRelayout 与CustomSingleChildLayout的同款重新布局判断方式"),
      const LabelText("- getSize 根据父组件给的约束计算自身尺寸，默认取约束的最大值"),
      const LabelText("- performLayout 布局子组件，在这个方法里，我们通过layoutChild、"
          "positionChild来测量与放置子组件"),
      AppSpacer.large,
      const NoticeText("更加复杂的自定义需求我们可以通过自定义RenderObject来实现，不过基本"
          "用不到，CustomMultiChildLayout与LayoutBuilder可以实现99%的自定义布局"),
    ];
  }
}

///本例中我们尝试通过自定义布局来实现游戏技能键的排布
class _LayoutBuilderExample extends StatefulWidget {
  final List<Widget> children;

  const _LayoutBuilderExample(this.children);

  @override
  State<StatefulWidget> createState() {
    return _LayoutBuilderExampleState();
  }
}

class _LayoutBuilderExampleState extends State<_LayoutBuilderExample> {
  static const defaultSize = Size(120, 120);

  final _measurementKey = GlobalKey();
  List<Widget>? widgetChildren;

  var _hasMeasured = false;
  var _childrenSize = Size.zero;

  void measureChildrenSize() async {
    if (!_hasMeasured) {
      //监听下一帧刷新，获取尺寸信息
      WidgetsBinding.instance.addPostFrameCallback((timestamp) {
        var renderStack =
            _measurementKey.currentContext?.findRenderObject() as RenderStack;
        //计算尺寸信息
        _childrenSize = Size.zero;
        renderStack.visitChildren((child) {
          if (child is RenderBox) {
            if (_childrenSize.width < child.size.width ||
                _childrenSize.height < child.size.height) {
              _childrenSize = Size(
                max(_childrenSize.width, child.size.width),
                max(_childrenSize.height, child.size.height),
              );
            }
          }
        });
        setState(() {
          _hasMeasured = true;
        });
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    ///在布局时我们通常需要知道子组件的大小，这里因为是基于LayoutBuilder实现，它的回调发生在
    ///Element mount或者update阶段，此时子组件还未测量，无法得知他们的尺寸，这里预先测量一
    ///遍子组件的尺寸，以便我们计算布局位置
    if (widgetChildren != widget.children) {
      //widget更新，我们重新测量按钮大小
      widgetChildren = widget.children;
      _hasMeasured = false;
      measureChildrenSize();
    }
    if (!_hasMeasured) {
      //临时插入到Widget树种进行测量，Offstage使得组件在组件树中不可见但可以测量尺寸
      return Offstage(
        child: Stack(
          key: _measurementKey,
          children: widget.children,
        ),
      );
    } else {
      return buildDisplayWidget();
    }
  }

  ///构建显示的组件
  Widget buildDisplayWidget() {
    return LayoutBuilder(builder: (context, constraints) {
      //根据父组件给的约束条件我们参考自身内容尺寸来决定最终组件的大小
      //我们这里比较想要一个120x120的空间用来放置组件，最好维持宽高比
      //使用constrainSizeAndAttemptToPreserveAspectRatio非常合适，如果有比较复杂的需求
      //则需要自己结合constrainWidth、constrainHeight来计算期望的尺寸
      final size =
          constraints.constrainSizeAndAttemptToPreserveAspectRatio(defaultSize);
      //取短边作为半径
      double radius = min(
          size.width - _childrenSize.width, size.height - _childrenSize.height);
      var spaceRadians = 0.0;
      if (widget.children.length > 1) {
        spaceRadians = pi / 2 / (widget.children.length - 1);
      }
      return SizedBox(
        width: size.width,
        height: size.height,
        child: Stack(
          children: List.generate(
            widget.children.length,
            (index) {
              return Positioned(
                right: radius * cos(index * spaceRadians),
                bottom: radius * sin(index * spaceRadians),
                width: _childrenSize.width,
                height: _childrenSize.height,
                child: Center(
                  child: widget.children[index],
                ),
              );
            },
          ),
        ),
      );
    });
  }
}

///实现居中代理对象，将子组件放置到CustomSingleChildLayout中心
class _CenterDelegate extends SingleChildLayoutDelegate {
  ///对比旧的代理对象，判断是否需要重新布局
  @override
  bool shouldRelayout(covariant SingleChildLayoutDelegate oldDelegate) {
    return oldDelegate is! _CenterDelegate;
  }

  ///获取CustomSingleChildLayout组件尺寸，默认的实现是取得约束内的最大尺寸
  @override
  Size getSize(BoxConstraints constraints) => constraints.biggest;

  ///根据父组件给予我们的约束，我们来决定child应该受到的约束，默认直接采用父组件给予的约束
  ///这里我们通过loosen让子组件可以在容器的范围内选取它自己的想要的宽高（在这个例子里外部设置的
  ///子组件是100x100）
  @override
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) =>
      constraints.loosen();

  ///获取child的偏移量，这是我们布局子组件的关键，要实现居中我们还要知晓子组件的尺寸，这个方法在
  ///performLayout中调用，在调用之前已经调用过child.layout传入我们设置的约束
  ///（getConstraintsForChild），所以此时CustomSingleChildLayout与子组件的尺寸已知
  ///如此，我们经过简单计算即可实现子组件居中
  @override
  Offset getPositionForChild(Size size, Size childSize) => Offset(
      (size.width - childSize.width) / 2, (size.height - childSize.height) / 2);
}

///实现与_LayoutBuilderExample一样的效果
class _MobileGameSkillKeyboardDelegate extends MultiChildLayoutDelegate {
  ///子组件的id
  final List<Object> childIds;

  _MobileGameSkillKeyboardDelegate(this.childIds);

  @override
  bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate) =>
      oldDelegate is! _MobileGameSkillKeyboardDelegate ||
      childIds != oldDelegate.childIds;

  ///老规矩，在约束条件内尽可能保持1:1的尺寸
  @override
  Size getSize(BoxConstraints constraints) => constraints
      .constrainSizeAndAttemptToPreserveAspectRatio(const Size(120, 120));

  ///布局子组件，在这个方法里，我们通过layoutChild、positionChild来测量与放置子组件
  @override
  void performLayout(Size size) {
    //1、测量子组件，这里我们允许子组件尺寸在我们的CustomMultiChildLayout容器内自适应大小
    final wrapContentConstrains = BoxConstraints.loose(size);
    var childrenSize = Size.zero;
    //遍历测量所有子组件，取他们的公共尺寸（假设每个子组件尺寸不同，以公共尺寸作为容器来居中显示他们）
    final childSizes = List.generate(
      childIds.length,
      (index) {
        final childSize = layoutChild(childIds[index], wrapContentConstrains);
        if (childrenSize.width < childSize.width ||
            childrenSize.height < childSize.height) {
          childrenSize = Size(
            max(childrenSize.width, childSize.width),
            max(childrenSize.height, childSize.height),
          );
        }
        return childSize;
      },
    );
    //2、放置子组件
    //取短边作为半径
    double radius =
        min(size.width - childrenSize.width, size.height - childrenSize.height);
    var spaceRadians = 0.0;
    if (childIds.length > 1) {
      spaceRadians = pi / 2 / (childIds.length - 1);
    }
    //先定位到右下角，方便根据三角函数直接计算偏移
    final centerOffset = Offset(
        size.width - childrenSize.width, size.height - childrenSize.height);
    for (int i = 0; i < childIds.length; i++) {
      positionChild(
        childIds[i],
        centerOffset
            .translate(
              //根据三角函数，将公共尺寸的子组件按圆周排布
              -radius * cos(spaceRadians * i),
              -radius * sin(spaceRadians * i),
            )
            .translate(
              //在公共尺寸内，对子组件进行居中
              (childrenSize.width - childSizes[i].width) / 2,
              (childrenSize.height - childSizes[i].height) / 2,
            ),
      );
    }
  }
}
